package com.espirit.eap.conversion;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.espirit.eap.pagelayout.Element;
import com.espirit.eap.pagelayout.ElementService;
import com.espirit.eap.pagelayout.TagCategory;
import com.espirit.eap.pagelayout.TagInfo;
import com.espirit.eap.pagelayout.ElementService.ElementCallback;
import com.espirit.eap.pagelayout.Layout;
import com.espirit.eap.util.TaglibManager;
import com.espirit.eap.util.tagAdaptor.TagAdaptorUtil;
import com.espirit.eap.util.tagAdaptor.TagElementAdaptor;
import com.googlecode.cswish.model.PageElement;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.util.FormatUtils;

public class ConversionCallback implements ElementCallback {

	private static final Log logger = LogFactory.getLog(ConversionCallback.class);
	
	private static final char[] FREEMARKER_ESCAPE_CHARS = new char[] {'"', '\'', '\\'};
	
	private ConversionContext conversionContext;
	
	// service
	private ElementService elementService;
	private TaglibManager tagManager;
	private TagAdaptorUtil tagAdaptorUtil;
	private boolean generateDebugInfo;
	
	// conversion information
	private StringBuilder layoutContent;
	private Map<String, Object> context;
	private Set<String> tagCategories;
	private Layout root;
	
	// the page element of current tag
	private Map<Element, PageElements> elementsMapping;
	
	// the flag of self-defined page
	private boolean isPageDefine;
	private boolean isDebug;
	
	public ConversionCallback(ConversionContext conversionContext) {
		this.conversionContext = conversionContext;
		
		this.layoutContent = conversionContext.getLayoutContent();
		this.root = conversionContext.getRoot();
		this.context = conversionContext.getContext();
		this.tagCategories = conversionContext.getTagCategories();
		this.isPageDefine = conversionContext.isPageDefine();
		this.isDebug = conversionContext.isDebug();
		
		this.elementsMapping = new HashMap<Element, PageElements>();
		elementsMapping.put(null, conversionContext.getRootPageElements());
		
		this.elementService = conversionContext.getElementService();
		this.tagManager = conversionContext.getTagManager();
		this.tagAdaptorUtil = conversionContext.getTagAdaptorUtil();
		this.generateDebugInfo = conversionContext.isGenerateDebugInfo();
	}
	
	@Override
	public boolean process(Element parent, Element element, int deep) {
		TagInfo tagInfo = tagManager.getTagInfo(root, element, context, null);
		boolean continueScanChild = true;
		if (tagInfo != null) {
			TagCategory tagCategory = tagInfo.getTagCategory();
			if (tagCategory != null && tagCategory.getName() != null) {
				tagCategories.add(tagCategory.getName());
			}
			
			String tab = StringUtils.repeat("\t", deep);
			if (TagElementAdaptor.TAG_CODE.equals(tagInfo.getTagName())) {
				Map<String, Object> tagProperties = tagInfo.getTagProperties();
				boolean hideContainer = element.getName() != null && element.getName().indexOf(TagElementAdaptor.TAG_CODE_ANONYMOUS) >= 0;
				appendCode(!hideContainer, layoutContent, (String)tagProperties.get("name"), (String)tagProperties.get("value"), tab);
			} else if (TagElementAdaptor.TAG_TEMPLATE.equals(tagInfo.getTagName())) {
				// see TagBlockSectionAdaptor.updateToElement
				parseTemplateTag(element, tagInfo, deep);
			} else {
				boolean isContainerTag = element.getChildren() != null && element.getChildren().size() > 0;
				
				// append the additional category class
				String categoryClass = conversionContext.getCategoryClass(element);
				if (categoryClass != null) {
					String clazz = (String)tagInfo.getTagProperties().get("clazz");
					String mergeClass = conversionContext.mergeClass(clazz, categoryClass);
					tagInfo.getTagProperties().put("clazz", mergeClass);
				}
				int includeTagBginPos = -1;
				if ("include".equals(tagInfo.getTagName())) {
					includeTagBginPos = layoutContent.length();
				}
				
				// element information
				PageElement newElement = isContainerTag ? new PageElements() : new PageElement();
				newElement.setType(tagInfo.getTagName());
				String fullName = tagAdaptorUtil.getFullName(element);
				newElement.setName(fullName);
				Map<String, Object> tagProperties = tagInfo.getTagProperties();
				String label = (String)tagProperties.get("label");
				if (label != null && label.length() > 0) {
					newElement.setLabel(label);
				}
				Object url = tagProperties.get("url");
				if (url != null) {
					newElement.addExtraParams("url", url);
				}
				Object src = tagProperties.get("src");
				if (src != null) {
					newElement.addExtraParams("src", src);
				}
				Object labelProperty = tagProperties.get("labelProperty");
				if (labelProperty != null) {
					newElement.addExtraParams("labelProperty", labelProperty);
				}
				// TODO: more information
				
				PageElements parentElement = elementsMapping.get(parent);
				if (parentElement == null) {
					if (logger.isDebugEnabled()) {
						logger.debug("No mapping for element:" + parent.getName() + "?");
					}
					parentElement = elementsMapping.get(null);
				}
				parentElement.addElement(newElement);
				if (isContainerTag) {
					elementsMapping.put(element, (PageElements) newElement);
				}
				
				// append tag content
				layoutContent.append(tab).append("<@").append(tagInfo.getTagName());
				
				Map<String, Object> defaultProperties = tagInfo.getDefaultProperties();
				for (Entry<String, Object> keyValue : tagInfo.getTagProperties().entrySet()) {
					String key = keyValue.getKey();
					if (tagInfo.hasTagProperty(key) && !tagInfo.isDefaultValue(key)) {
						appendProperty(layoutContent, key, keyValue.getValue(), defaultProperties.get(keyValue.getKey()), tab);
					}
				}
				
				if (isContainerTag) {
					layoutContent.append(">");
					
					// debug info
					if (generateDebugInfo) {
						layoutContent.append("<#--").append(element.getClass().getSimpleName()).append(":").append(element.getId()).append("-->\n");
					} else {
						layoutContent.append('\n');
					}
					
					List children = element.getSortChildren();

					for (Object child : children) {
						Element ele = (Element) child;
						iterator(ele, this, deep + 1);
					}
					
					layoutContent.append(tab).append("</@").append(tagInfo.getTagName()).append(">\n");
				} else {
					layoutContent.append("/>");
					
					// debug info
					if (generateDebugInfo) {
						Element debugElement = element.getId() != null ? element : element;
						layoutContent.append("<#--").append(debugElement.getClass().getSimpleName()).append(":").append(debugElement.getId()).append("-->\n");
					} else {
						layoutContent.append('\n');
					}
				}
				
				if (includeTagBginPos != -1) {
					conversionContext.addReferencePage((String)tagInfo.getTagProperties().get("url"), includeTagBginPos, layoutContent.length());
				}
			}
			continueScanChild = false;
		}
		return continueScanChild;
	}

	private void parseTemplateTag(Element element, TagInfo tagInfo, int deep) {
		Map<String, Object> tagProperties = tagInfo.getTagProperties();
		String url = (String)tagProperties.get("url");
		String name = (String)tagProperties.get("name");
		String template = null;
		int begin = layoutContent.length();
		
		if (url != null && url.length() > 0) {
			// load template
			try {
				template = conversionContext.getTxFileService().read(null, url);
				
				List children = element.getSortChildren();
				// insert the children to the '[var]' of template 
				if (template != null && children != null && !children.isEmpty()) {
					Object[] args = new Object[children.size()];
					int i = 0;
					for (Object child : children) {
						Element ele = (Element) child;
						
						conversionContext.setLayoutContent(new StringBuilder());
						ConversionCallback callback = new ConversionCallback(conversionContext);						
						iterator(ele, callback, deep + 1);
						args[i++] = conversionContext.getLayoutContent().toString();
					}
					// restore the original content
					conversionContext.setLayoutContent(this.layoutContent);
					
					StringBuilder sbTemplate = new StringBuilder();
					int index = template.indexOf("[var]");
					int lastIndex = 0;
					int j = 0;
					while (index != -1) {
						sbTemplate.append(template.substring(lastIndex, index));
						if (j < args.length) {
							sbTemplate.append(args[j++]);
						}
						lastIndex = index + 5;		// 5 is the length of '[var]'
						index = template.indexOf("[var]", lastIndex);
					}
					if (lastIndex != -1) {
						sbTemplate.append(template.substring(lastIndex));
						template = sbTemplate.toString();
					}
				}
				
				//if (isPageDefine && template != null) {
					// remove <html>, <head>, <body> etc
					//template = template.replaceAll("<!DOCTYPE[\\s\\S]*?>|<html[\\s\\S]*?>|</html>|<head[\\s\\S]*?>|</head>|<body[\\s\\S]*?>|</body>", "");
				//}
			} catch (Exception ex) {
				logger.error("Failed to reald template tag: " + url, ex);
			}
		} else {
			template = null;
		}
		
		Object contain = tagProperties.get(TagElementAdaptor.TAG_TEMPLATE_CONTAIN);
		boolean templateContainTagInfo = "true".equals(contain) || Boolean.TRUE.equals(contain);
		if (!templateContainTagInfo) {
			String tab = "";
			if (isPageDefine) {
				tab = StringUtils.repeat("\t", deep);
				layoutContent.append(tab).append("<div class='stdEle stdCtn jtemplate' uiType='template' templateContainTagInfo='false' fullName='")
					.append(name).append('\'');
				
				for (Entry<String, Object> entry : tagProperties.entrySet()) {
					String key = entry.getKey();
					if ("tagName".equals(key) || "name".equals(key) || "url".equals(key) || "positionCol".equals(key)) {
						continue;
					}
					
					Object value = entry.getValue();
					String strValue = value == null ? null : value.toString();
					if (strValue != null && strValue.length() > 0) {
						layoutContent.append(' ').append(key).append(" ='").append(strValue).append('\'');
					}
				}
				
				layoutContent.append(" url='").append(url).append("'");
				
				layoutContent.append(">\n");
			}
			if (template != null) {
				layoutContent.append(template);
			}
			if (isPageDefine) {
				layoutContent.append('\n').append("</div>\n");
			}
		} else {
			if (template != null) {
				layoutContent.append(template).append('\n');
			}
		}
		conversionContext.addReferencePage(url, begin, layoutContent.length());
	}
	
	private void appendCode(boolean showContainer, StringBuilder sb, String name, String value, String tab) {
		if (isPageDefine && showContainer) {
			sb.append("\n").append("<div uiType='code' class='jcode stdEle refEle' name='").append(name).append("'>");
			// NOTICE: don't add the tab format
			String content;
			if (isDebug) {
				content = "Debug Model";
			} else {
				content = value;
			}
			sb.append(content).append("</div>\n");
		} else {
			sb.append(tab).append(StringUtils.replace(value, "\n", "\n" + tab)).append('\n');
		}
	}
	
	private void appendProperty(StringBuilder sb, String property, Object value, Object defaultValue, String tab) {
		if (value != null) {
			sb.append(' ');
			if (value instanceof Boolean) {
				sb.append(property).append('=').append(value);
			} else if (value instanceof Number) {
				sb.append(property).append('=').append(value);
			} else if (value instanceof Collection){
				Collection coll = (Collection) value;
				if (coll.size() > 0) {
					String val = coll.iterator().next().toString();
					if (val.length() > 0 && val.charAt(0) == '{') {		// MAP
						String sValue = StringUtils.join(coll, ",\n" + tab + "\t\t");
						sb.append(property).append("=[").append(sValue).append("]\n\t").append(tab);
					} else {											// String
						String sValue = StringUtils.join(coll, "',\n" + tab + "\t\t'");
						sb.append(property).append("=['").append(sValue).append("']\n\t").append(tab);
					}
				} else {
					sb.append(property).append("=[]");
				}
			} else {
				if (defaultValue != null && !(defaultValue instanceof String)) {
					// treat the value as variable if the value is string type and
					// the default value is not string type
					sb.append(property).append('=').append(value);
				} else {
					String str = (String)value;
					if (str != null && str.length() > 0) {
						// '%' means the string is expression
						if (str.charAt(0) == '%' && !this.isDebug) {
							str = str.substring(1);
							sb.append(property).append("=").append(str);
						} else {
							// append string value
							str = FormatUtils.escapeWithSlash((String)value, FREEMARKER_ESCAPE_CHARS);
							sb.append(property).append("='").append(str).append('\'');
						}
					} else {
						// append empty string value
						sb.append(property).append("=''");
					}
				}
			}
		}
	}
	
	private void iterator(Element element, ElementCallback callback, int deep) {
		elementService.iteratorElement(element.getParent(), element, callback, deep);
	}
}